<?php

Raise::load('core.RaiseObject');
Raise::load('db.core.RaiseDbConnection');
Raise::load('core.reflection.RaiseVariable');

/**
 * RaiseDbCommand class
 * Data Access Command Object that sends command to the data access server via
 * the assosciated connection.
 *
 * @author Sam-Mauris Yong / hellclanner at live dot com
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package phpraise.db.core
 * @since 1.2
 */
class RaiseDbCommand extends RaiseObject {

    /**
     * The SQL query assosciated with this RaiseDbCommand object
     * @var string
     */
    private $command;

    /**
     * An array of parameters that will be binded into the query string
     * @var RaiseCollection
     */
    private $params;

    /**
     * The RaiseDbConnection assosciated with this RaiseDbCommand object
     * @var RaiseDbConnection
     */
    private $connection;


    /**
     * Create a new RaiseDbCommand based on the connection
     * @param RaiseDbConnection $conn The connection assosciated
     * @param string|RaiseDbSqlSelect $cmd The command to execute
     */
    public function __construct($conn, $cmd = ''){
        if(is_object($cmd)){
            $cmd = $cmd->__toString();
        }
        $this->command = $cmd;
        if($conn instanceof RaiseDbConnection){
            $this->connection = $conn;
        }else{
            throw new RaiseInvalidArgumentException('RaiseDbCommand::__construct() expecting $conn to be an instance of RaiseDbConnection, ' . RaiseVariable::typeInfo($conn) . ' given instead.');
        }
        $this->params = new RaiseCollection();
    }

    /**
     * Get the command of this RaiseDbCommand
     * @return string
     */
    public function command(){
        return $this->command;
    }

    /**
     * Get the connection of this command
     * @return RaiseDbConnection
     */
    public function connection(){
        return $this->connection;
    }

    /**
     * Get or set the collection of parameters
     * @param RaiseCollection $params
     * @return RaiseCollection
     */
    public function params($params = null){
        if(func_num_args() == 1){
            if(!($params instanceof RaiseCollection)){
                throw new RaiseInvalidArgumentException('Parameters collection expecting RaiseCollection');
            }
            $this->params = $params;
        }
        return $this->params;
    }

    /**
     * Escape a string using the appropriate functions
     * @param string $s
     * @return string
     */
    private function escapeString($s){
        return $this->connection->adapter()->escapeString($s);
    }

    /**
     * Parse and prepare a parameter value
     * @param mixed $v
     * @return string
     */
    private function parseParameterValue($v){
        switch(RaiseVariable::dataType($v)){
            case RaiseVariable::TYPEARRAY:
                foreach($v as &$a){
                    $a = $this->parseParameterValue($a);
                }
                $v = implode(',', $v);
                break;
            case RaiseVariable::TYPEBOOLEAN:
                $v = RaiseConvert::to(RaiseConvert::TO_BOOLEAN, $v) ? 1 : 0;
                break;
            case RaiseVariable::TYPEINTEGER:
                $v = RaiseConvert::to(RaiseConvert::TO_INTEGER, $v);
                break;
            case RaiseVariable::TYPEDOUBLE:
                $v = RaiseConvert::to(RaiseConvert::TO_DOUBLE, $v);
                break;
            case RaiseVariable::TYPESTRING:
                $v = '\'' . $this->escapeString($v) . '\'';
                break;
            case RaiseVariable::TYPENULL:
                $v = 'NULL';
                break;
            case RaiseVariable::TYPEOBJECT:
                if($v instanceof RaiseDbCommand){
                    $v = $v->parseCommandToQuery();
                }elseif($v instanceof RaiseDateTime){
                    $v = $this->parseParameterValue($v->toISO8601());
                }else{
                    $v = $this->parseParameterValue(RaiseConvert::to(RaiseConvert::TO_STRING, $v));
                }
                break;
        }
        return $v;
    }

    /**
     * Parse the command and enter the parameters, then return the SQL string
     * @return string the SQL query parsed
     */
    private function parseCommandToQuery(){
        $sql = $this->command;
        $params = $this->params()->raiseClone();
        foreach($params as $k => $v){
            $params->add($k, $this->parseParameterValue($v));
        }

        $sql = str_replace($params->keys()->toArray(), $params->values()->toArray(), $sql);
        return $sql;
    }

    /**
     * Execute the query and return the result
     * @param string $q The SQL query to run
     * @return resource The result that is returned from the query
     */
    private function query($q){
        $this->connection->adapter()->query($q);
        if(!$this->connection->result()){
            throw new RaiseDatabaseSQLException('A query / update SQL command failed to execute.', $q, $this->connection->adapter()->lastError());
        }
        return $this->connection->result();
    }

    /**
     * Execute as a non-query SQL command
     * @return boolean TRUE on successful operation, FALSE otherwise.
     */
    public function executeNonQuery(){
        if($this->connection->connect()){
            return RaiseConvert::to(RaiseConvert::TO_BOOLEAN, $this->query($this->parseCommandToQuery()));
        }
        throw new RaiseNoConnectionMadeException('RaiseDbCommand::executeNonQuery() requires a connection made to the database in order to execute the command.');
    }

    /**
     * Execute as a scalar SQL command and return the first value
     * @return mixed The scalar value is returned if successful, NULL if no matching row is found.
     */
    public function executeScalar(){
        if($this->connection->connect()){
            $this->query($this->parseCommandToQuery());

            $row = $this->connection->adapter()->fetchRowArray();

            if(is_array($row)){
                return current($row);
            }else{
                return null;
            }
        }
        throw new RaiseNoConnectionMadeException('RaiseDbCommand::executeScalar() requires a connection made to the database in order to execute the command.');
    }

    /**
     * Execute the command and fetch as a single object
     * @param string $class (optional) The class the returned object should be
     * @return object|boolean The object is returned if successful, NULL if no object found.
     */
    public function executeFetchSingleObject($class = 'stdClass'){
        if($this->connection->connect()){
            $this->query($this->parseCommandToQuery());

            $obj = $this->connection->adapter()->fetchRowObject($class);

            if($obj){
                return $obj;
            }else{
                return null;
            }
        }
        throw new RaiseNoConnectionMadeException('RaiseDbCommand::executeFetchSingleObject() requires a connection made to the database in order to execute the command.');
    }

    /**
     * Execute and return an array of object based on the result
     * @param string $class (optional) The class the returned objects should be
     * @return array|boolean The array of objects is returned if successful, FALSE otherwise.
     */
    public function executeFetchObject($class = 'stdClass'){
        if($this->connection->connect()){
            $this->query($this->parseCommandToQuery());

            $table = array();

            while($obj = $this->connection->adapter()->fetchRowObject($class)){
                $table[] = $obj;
            }

            return $table;
        }
        throw new RaiseNoConnectionMadeException('RaiseDbCommand::executeFetchObject() requires a connection made to the database in order to execute the command.');
    }

    /**
     * Execute and fetch the first row from result as a single dimension array
     * @return array|boolean The array table is returned if successful, FALSE otherwise.
     */
    public function executeFetchSingleArray(){
        if($this->connection->connect()){
            $this->query($this->parseCommandToQuery());

            $obj = $this->connection->adapter()->fetchRowArray();

            if($obj){
                return $obj;
            }else{
                return false;
            }
        }
        throw new RaiseNoConnectionMadeException('RaiseDbCommand::executeFetchSingleArray() requires a connection made to the database in order to execute the command.');
    }

    /**
     * Execute and fetch result table as a two dimension array
     * @return array|boolean The array table is returned if successful, FALSE otherwise.
     */
    public function executeFetchArrayTable(){
        if($this->connection->connect()){
            $this->query($this->parseCommandToQuery());

            $table = array();

            while($row = $this->connection->adapter()->fetchRowArray()){
                $table[] = $row;
            }

            return $table;
        }
        throw new RaiseNoConnectionMadeException('RaiseDbCommand::executeFetchArrayTable() requires a connection made to the database in order to execute the command.');
    }

}
